package de.dqi.pong.engine;

import static de.dqi.pong.engine.Direction.*;
import de.dqi.pong.AILoader;
import de.dqi.pong.graphics.Render;
import de.dqi.pong.menu.Frame;


/**
 * TODO Tick - finish class definition
 * <hr>
 * 
 * <hr><dl><dt>
 * Summary: </dt><br><dd>
 *  
 * </dd><br><dt>
 * Author: </dt><br><dd>
 *  Vincent
 * </dd></dl>
 */
public class Tick
		implements Runnable {
	
	private Ball			ball;
	private Bat				batLeft;
	private Bat				batRight;
	private Playfield		field;
	private Render			render;
	private AI				aiLeft;
	private Thread			leftThread;
	private AI				aiRight;
	private Thread			rightThread;
	
	public static final int	fps				= 30;
	private long			startTime;
	private long			lastTime;
	private long			currentTime;
	private long			deltaTime		= 0;
	private final double	baseSpeed		= 1;
	private final double	speedGain		= 0.00002;
//	private final double	speedGain		= 0.0002;
	private double			speedup			= 1;
	private final int		timeout			= 350;
	private int				nextSwap;
	private Bat				nextSpawn;
	private final int		scoreTillSwap	= 5;
	private final int		rounds			= 10;					//Edit
	private final double	maxAngle		= Math.toRadians(70);
	private boolean			running;
	
	private String			winner;								//TODO mark winner differently
																	
																	
	public Tick() {
		field = new Playfield();
		
		render = new Render();
	}
	
	/*
	 * (non-Javadoc)
	 * 
	 * @see java.lang.Runnable#run()
	 */
	@Override
	public void run() {
		startTime = System.currentTimeMillis();
		lastTime = startTime;
		currentTime = startTime;
		deltaTime = 0;
		ball = new Ball();
		batLeft = new Bat(Bat.NORMALLEFT, 0);
		batRight = new Bat(Bat.NORMALRIGHT, field.width);
		
		batLeft.span = 3;
		batRight.span = batLeft.span;
		
		ball.spawnAt = batRight;
		nextSpawn = batLeft;
		nextSwap = 5;
		speedup = 1;
		
		InitializationNotification init = new InitializationNotification(
				field.width, field.height,
				ball.radius, batLeft.span);
		render.initialize(init);
		aiLeft.initialize(init, true);
		aiRight.initialize(init, false);
		
		//to prevent null pointer errors
		TickNotification tickNote = new TickNotification(
				ball.xPos, ball.yPos,
				batLeft.yPos, batRight.yPos,
				batLeft.score, batRight.score);
		aiLeft.update(tickNote);
		aiRight.update(tickNote);
		render.drawFrame(tickNote);
		
		leftThread.start();
		rightThread.start();
		
		loop();
		
		Frame.getInstance().switchPanel(Frame.ENDMENU);
	}
	
	private void loop() {
		running = true;
		while (running) {
			currentTime = System.currentTimeMillis();
			deltaTime = currentTime - lastTime;
			speedup = baseSpeed + speedGain * (currentTime - startTime);
			
			moveBats();
			if (ball.isOut) {
				respawnBall();
			}
			moveBall();
			
			TickNotification tickNote = new TickNotification(
					ball.xPos, ball.yPos,
					batLeft.yPos, batRight.yPos,
					batLeft.score, batRight.score);
			aiLeft.update(tickNote);
			aiRight.update(tickNote);
			render.drawFrame(tickNote);
			
			
			try {
				long wait = (1000 / fps) - (System.currentTimeMillis() - currentTime);
				if (wait > 0) {
					Thread.sleep(wait);
				}
			} catch (InterruptedException e) {
				System.err.println("Wait interval interrupted");
			}
			lastTime = currentTime;
		}
		
	}
	
	private void moveBats() {
		if (aiLeft.direction() == Up) {
			batLeft.yPos++;
		} else if (aiLeft.direction() == Down) {
			batLeft.yPos--;
		}
		if (aiRight.direction() == Up) {
			batRight.yPos++;
		} else if (aiRight.direction() == Down) {
			batRight.yPos--;
		}
		
		//Move back inbounds
		if (batLeft.yPos > field.height - batLeft.span) {
			batLeft.yPos = field.height - batLeft.span;
		} else if (batLeft.yPos < batLeft.span) {
			batLeft.yPos = batLeft.span;
		}
		if (batRight.yPos > field.height - batRight.span) {
			batRight.yPos = field.height - batRight.span;
		} else if (batRight.yPos < batRight.span) {
			batRight.yPos = batRight.span;
		}
	}
	
	private void respawnBall() {
		ball.outFor -= deltaTime;
		
		if (ball.outFor <= 0) {
			ball.isOut = false;
			ball.outFor = 0;
			startTime = currentTime;
			
			ball.yVel = 0;
			ball.xVel = ball.spawnAt.normal;
			ball.xPos = ball.spawnAt.xPos + ball.spawnAt.normal;
			ball.yPos = ball.spawnAt.yPos;
			
			System.out.println("Respawn@ " + ball.xPos + "|" + ball.yPos);
		}
	}
	
	private void moveBall() {
		
		boolean didBounceY = false;
		double random = Math.random();
		double deltaX = ball.xVel * speedup;
		double deltaY = ball.yVel * speedup;
		double newX = ball.xPos + deltaX;
		double newY = ball.yPos + deltaY;
		
		if (!ball.isOut) {
			Bat bat = null;
			//Bat Right
			if (newX < batLeft.xPos) {
				bat = batLeft;
			} else if (newX > batRight.xPos) {
				bat = batRight;
			}
			if (bat != null) {
				double angle = Math.asin(ball.yVel);
				double xPre = bat.xPos - ball.xPos;
				double yPre = deltaY * xPre / deltaX;
				double yHit = ball.yPos + yPre;
				double over = (deltaX - xPre) * deltaX / speedup;
				double yCor = 0;
				
				//Check for ceiling/floor bounces just before the 
				if (yHit > field.height - ball.radius) {
					yCor = 2 * (field.height - ball.radius - newY);
				} else if (yHit < ball.radius) {
					yCor = -2 * (newY - ball.radius);
				}
				if (yCor != 0) {
					newY += yCor;
					yPre *= -1;
					yHit = ball.yPos + yPre;
					ball.yVel *= -1;
					deltaY *= -1;
					angle = Math.asin(ball.yVel);
					didBounceY = true;
				}
				
				if (yHit <= bat.yPos + bat.span && yHit >= bat.yPos - bat.span) {
					if (yHit > bat.yPos + bat.span / 3) {
//						System.out.print("Lower ");
						angle = random * maxAngle + (1 - random) * angle;
					} else if (yHit < bat.yPos - bat.span / 3) {
//						System.out.print("Upper ");
						angle = random * -1 * maxAngle + (1 - random) * angle;
					} else {
//						System.out.print("Norm. ");
					}
					ball.xVel = bat.normal * Math.cos(angle);
					ball.yVel = Math.sin(angle);
					newX = bat.xPos + ball.xVel * over;
					newY = yHit + ball.yVel * over;
					//info
					String fill = "";
					if (Math.signum(angle) == 1) {
						fill = " ";
					}
//					System.out.println("Bounce@ " + fill + (int) Math.toDegrees(angle) + "° [" + newX + "|"
//							+ newY + "]");
				} else {
					didBounceY = true;
					score(bat);
				}
			}
			
			
			//Bounce off upper/lower edge
			double yCor = 0;
			if (!didBounceY && newY > field.height - ball.radius) {
				newY -= 2 * (newY - (field.height - ball.radius));
				ball.yVel = ball.yVel * -1;
//				System.out.println("Floor@ " + "[" + newX + "|" + newY + "]");
			} else if (!didBounceY && newY < 0 + ball.radius) {
				newY -= 2 * (newY - ball.radius);
				ball.yVel = ball.yVel * -1;
//				System.out.println("Ceiling@ " + " [" + newX + "|" + newY + "]");
			}
		}
		
		ball.xPos = newX;
		ball.yPos = newY;
	}
	
	private void score(Bat missed) {
		Bat scorer;
		if (missed == batLeft) {
			scorer = batRight;
		} else {
			scorer = batLeft;
		}
		scorer.score++;
		
		ball.isOut = true;
		ball.outFor = timeout;
		
		System.out.println("Scored! " + batLeft.score + ":" + batRight.score);
		
		if (scorer.score == 2 * scoreTillSwap * rounds) {
			running = false;
			if (scorer == batRight) {
				winner = aiRight.getClass().getSimpleName();
				winner = winner + " (right)";
				System.out.println("\nRight side has won!");
			} else {
				winner = aiLeft.getClass().getSimpleName();
				winner = winner + " (left)";
				System.out.println("\nLeft side has won!");
			}
		} else if (scorer.score == nextSwap) {
			ball.spawnAt = nextSpawn;
			if (nextSpawn == batRight) {
				System.out.println("When " + (nextSwap + 5) + " Points are scored, the ball will spawn on the left");
				nextSpawn = batLeft;
			} else {
				System.out.println("When " + (nextSwap + 5) + " Points are scored, the ball will spawn on the right");
				nextSpawn = batRight;
			}
			nextSwap += scoreTillSwap;
		} else {
			ball.spawnAt = scorer;
		}
		
		System.out.println();
	}
	
	public void loadAIs(int idLeft, int idRight) {
		if (!running) {
			aiLeft = AILoader.newInstanceOf(idLeft);
			leftThread = new Thread(aiLeft, AILoader.getLoadedAIs()[idLeft] + " (left)");
			aiRight = AILoader.newInstanceOf(idRight);
			rightThread = new Thread(aiRight, AILoader.getLoadedAIs()[idRight] + " (right)");
		}
	}
	
	public Render getRender() {
		return render;
	}
	
	public double getWidth() {
		return field.width;
	}
	
	public double getHeight() {
		return field.height;
	}
	
	
	/**
	 * @return the winner
	 */
	public String getWinner() {
		return winner;
	}
	
	public String getScore() {
		return Integer.toString(batLeft.score) + ":" + Integer.toString(batRight.score);
	}
	
}